import java.io.*;
import java.util.*;

public class DocumentsProcessor implements IDocumentsProcessor {

    @Override
    public Map<String, List<String>> processDocuments(String directoryPath, int n) {

        if (n < 1) {
            return new HashMap<>();
        }

        Map<String, List<String>> nGramsMap = new HashMap<>();
        File dir = new File(directoryPath);
        File[] files = dir.listFiles();

        // loop through all files in the directory
        if (files != null) {
            for (File file : files) {
                if (file.isFile()) {
                    // BufferedReader for better performance
                    try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
                        nGramsMap.put(file.getName(), this.generateNGrams(reader, n));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return nGramsMap;
    }

    /**
     * generates n-grams from the document
     *
     * @return list of n-grams
     */
    private List<String> generateNGrams(Reader reader, int n) throws IOException {
        // create iterator
        DocumentIterator iterator = new DocumentIterator(reader, n);

        List<String> nGrams = new ArrayList<>();

        // generates and add n-gram
        while (iterator.hasNext()) {
            nGrams.add(iterator.next());
        }

        return nGrams;
    }

    @Override
    public List<Tuple<String, Integer>> storeNGrams(Map<String, List<String>> docs,
                                                    String nwordFilePath) {

        List<Tuple<String, Integer>> result = new ArrayList<>();

        // try-with-resources, automatically closes the writer
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(nwordFilePath))) {
            int totalDocs = docs.size();
            int currentDoc = 0;

            // iterate through each document's n-grams
            for (Map.Entry<String, List<String>> entry : docs.entrySet()) {
                String fileName = entry.getKey();
                List<String> nGrams = entry.getValue();

                // calculate the byte size of the n-grams
                int byteSize = nGrams
                        .stream()
                        .mapToInt(String::length)
                        .sum() + nGrams.size();

                // join the n-grams, internally uses StringBuilder
                String nGramsString = String.join(" ", nGrams);

                writer.write(nGramsString);
                writer.flush();

                if (++currentDoc < totalDocs) {
                    writer.write(" ");
                }

                result.add(new Tuple<>(fileName, byteSize));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }

    @Override
    public TreeSet<Similarities> computeSimilarities(String nwordFilePath,
                                                     List<Tuple<String, Integer>> fileindex) {
        TreeSet<Similarities> similaritiesSet = new TreeSet<>();
        int currentFileIdx = 0; // current file index

        try (BufferedReader reader = new BufferedReader(new FileReader(nwordFilePath))) {
            int targetSize = fileindex.get(currentFileIdx).getRight(); // target byte size
            int totalBytesRead = 0; // total bytes

            Map<String, Set<String>> ngramFiles = new HashMap<>();
            int c;

            while ((c = reader.read()) != -1) {
                String word = readNextWord(reader, c); // read next

                if (word != null) {
                    totalBytesRead += word.length() + 1;

                    // handle the word, update ngramFiles
                    Set<String> files = ngramFiles.computeIfAbsent(word, k -> new HashSet<>());
                    String currentFile = fileindex.get(currentFileIdx).getLeft();

                    if (!files.isEmpty()) {
                        if (!files.contains(currentFile)) {
                            for (String otherFile : files) {
                                Similarities searchKey = new Similarities(otherFile, currentFile);
                                Similarities existing = similaritiesSet.floor(searchKey);
                                // if similar n-gram exists, increase count
                                if (existing != null && existing.equals(searchKey)) {
                                    similaritiesSet.remove(existing);
                                    existing.setCount(existing.getCount() + 1);
                                    similaritiesSet.add(existing);
                                } else {
                                    // no similar n-gram, create new one
                                    searchKey.setCount(1);
                                    similaritiesSet.add(searchKey);
                                }
                            }
                        }
                    }

                    files.add(currentFile);
                }

                // check if we've read enough bytes
                if (totalBytesRead >= targetSize) {
                    currentFileIdx++;
                    if (currentFileIdx < fileindex.size()) {
                        targetSize = fileindex.get(currentFileIdx).getRight();
                        totalBytesRead = 0; // reset
                    } else {
                        break;
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return similaritiesSet;
    }

    private String readNextWord(BufferedReader reader, int currentChar) throws IOException {

        StringBuilder word = new StringBuilder();

        while (Character.isLetter((char) currentChar)) {
            word.append((char) currentChar);
            currentChar = reader.read();
            if (currentChar == -1) {
                break;
            }
        }

        return word.length() > 0 ? word.toString() : null;
    }

    @Override
    public void printSimilarities(TreeSet<Similarities> sims, int threshold) {
        // sort by count, file1, file2, desc
        Comparator<Similarities> countComparator = (s1, s2) -> {
            int countCompare = Integer.compare(s2.getCount(), s1.getCount());
            if (countCompare != 0) {
                return countCompare;
            }
            int file1Compare = s1
                    .getFile1()
                    .compareTo(s2.getFile1());
            if (file1Compare != 0) {
                return file1Compare;
            }
            return s1
                    .getFile2()
                    .compareTo(s2.getFile2());
        };

        // print
        sims
                .stream()
                .filter(s -> s.getCount() > threshold)
                .sorted(countComparator)
                .forEach(s -> System.out.println(s.getFile1() + s.getFile2() + s.getCount()));
    }

    List<Tuple<String, Integer>> processAndStore(String directoryPath, String sequenceFile, int n) {
        if (n < 1) {
            return new ArrayList<>();
        }

        List<Tuple<String, Integer>> result = new ArrayList<>();
        File dir = new File(directoryPath);
        File[] files = dir.listFiles();

        if (files == null) {
            return result;
        }

        // loop through all files in the directory
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(sequenceFile))) {
            for (File file : files) {
                if (!file.isFile()) {
                    continue;
                }

                try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
                    DocumentIterator iterator = new DocumentIterator(reader, n);
                    StringBuilder ngramsLine = new StringBuilder();
                    int byteSize = 0;
                    boolean firstNgram = true;

                    // iterate through the document
                    while (iterator.hasNext()) {
                        String ngram = iterator.next();

                        if (!firstNgram) {
                            ngramsLine.append(" ");
                        }

                        ngramsLine.append(ngram);
                        byteSize += ngram.length() + 1;
                        firstNgram = false;
                    }

                    // directly write to the file, no need to store it in memory
                    if (byteSize > 0) {
                        writer.write(ngramsLine.toString());
                        writer.newLine();
                        writer.flush();
                        result.add(new Tuple<>(file.getName(), byteSize));
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }
}
